/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

/**
 * @author Vladimir N. Molotkov, Alexander Y. Kleymenov
 */

package org.apache.harmony.security.x509;

import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.harmony.security.asn1.ASN1Implicit;
import org.apache.harmony.security.asn1.ASN1OctetString;
import org.apache.harmony.security.asn1.ASN1Sequence;
import org.apache.harmony.security.asn1.ASN1Type;
import org.apache.harmony.security.asn1.BerInputStream;
import org.apache.harmony.security.internal.nls.Messages;

/**
 * The class encapsulates the ASN.1 DER encoding/decoding work with the
 * following structure which is a part of X.509 certificate (as specified in RFC
 * 3280 - Internet X.509 Public Key Infrastructure. Certificate and Certificate
 * Revocation List (CRL) Profile. http://www.ietf.org/rfc/rfc3280.txt):
 * 
 * <pre>
 * 
 *   NameConstraints ::= SEQUENCE {
 *        permittedSubtrees       [0]     GeneralSubtrees OPTIONAL,
 *        excludedSubtrees        [1]     GeneralSubtrees OPTIONAL }
 * 
 *   GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree
 * 
 * </pre>
 * 
 * 
 * @see org.apache.harmony.security.x509.GeneralSubtree
 * @see org.apache.harmony.security.x509.GeneralName
 */
public class NameConstraints extends ExtensionValue {

	public static NameConstraints decode(byte[] encoding) throws IOException {
		return (NameConstraints) ASN1.decode(encoding);
	}

	// the value of permittedSubtrees field of the structure
	private final GeneralSubtrees permittedSubtrees;
	// the value of excludedSubtrees field of the structure
	private final GeneralSubtrees excludedSubtrees;

	// the ASN.1 encoded form of NameConstraints
	private byte[] encoding;
	// helper fields
	private ArrayList<GeneralName>[] permitted_names;

	private ArrayList<GeneralName>[] excluded_names;

	/**
	 * X.509 NameConstraints encoder/decoder.
	 */
	public static final ASN1Sequence ASN1 = new ASN1Sequence(new ASN1Type[] {
			new ASN1Implicit(0, GeneralSubtrees.ASN1),
			new ASN1Implicit(1, GeneralSubtrees.ASN1) }) {
		{
			setOptional(0);
			setOptional(1);
		}

		@Override
		protected Object getDecodedObject(BerInputStream in) {
			final Object[] values = (Object[]) in.content;
			return new NameConstraints((GeneralSubtrees) values[0],
					(GeneralSubtrees) values[1], in.getEncoded());
		}

		@Override
		protected void getValues(Object object, Object[] values) {

			final NameConstraints nc = (NameConstraints) object;

			values[0] = nc.permittedSubtrees;
			values[1] = nc.excludedSubtrees;
		}
	};

	/**
	 * Default ctor
	 */
	public NameConstraints() {
		this(null, null);
	}

	/**
	 * Constructs <code>NameConstrains</code> object
	 * 
	 * @param permittedSubtrees
	 *            : GeneralSubtrees
	 * @param excludedSubtrees
	 *            : GeneralSubtrees
	 */
	public NameConstraints(GeneralSubtrees permittedSubtrees,
			GeneralSubtrees excludedSubtrees) {
		if (permittedSubtrees != null) {
			final List<?> ps = permittedSubtrees.getSubtrees();
			if ((ps == null) || (ps.size() == 0)) {
				throw new IllegalArgumentException(
						Messages.getString("security.17D")); //$NON-NLS-1$
			}
		}
		if (excludedSubtrees != null) {
			final List<?> es = excludedSubtrees.getSubtrees();
			if ((es == null) || (es.size() == 0)) {
				throw new IllegalArgumentException(
						Messages.getString("security.17E")); //$NON-NLS-1$
			}
		}
		this.permittedSubtrees = permittedSubtrees;
		this.excludedSubtrees = excludedSubtrees;
	}

	//
	// Constructs NameConstrains object
	// @param permittedSubtrees: GeneralSubtrees
	// @param excludedSubtrees: GeneralSubtrees
	// @param encoding: byte[]
	//
	private NameConstraints(GeneralSubtrees permittedSubtrees,
			GeneralSubtrees excludedSubtrees, byte[] encoding) {
		this(permittedSubtrees, excludedSubtrees);
		this.encoding = encoding;
	}

	/**
	 * Places the string representation of extension value into the StringBuffer
	 * object.
	 */
	@Override
	public void dumpValue(StringBuffer buffer, String prefix) {
		buffer.append(prefix).append("Name Constraints: [\n"); //$NON-NLS-1$
		if (permittedSubtrees != null) {
			buffer.append(prefix).append("  Permitted: [\n"); //$NON-NLS-1$
			for (final Object name : permittedSubtrees.getSubtrees()) {
				((GeneralSubtree) name).dumpValue(buffer, prefix + "    "); //$NON-NLS-1$
			}
			buffer.append(prefix).append("  ]\n"); //$NON-NLS-1$
		}
		if (excludedSubtrees != null) {
			buffer.append(prefix).append("  Excluded: [\n"); //$NON-NLS-1$
			for (final Object name : excludedSubtrees.getSubtrees()) {
				((GeneralSubtree) name).dumpValue(buffer, prefix + "    "); //$NON-NLS-1$
			}
			buffer.append(prefix).append("  ]\n"); //$NON-NLS-1$
		}
		buffer.append('\n').append(prefix).append("]\n"); //$NON-NLS-1$
	}

	/**
	 * Returns ASN.1 encoded form of this X.509 NameConstraints value.
	 * 
	 * @return a byte array containing ASN.1 encode form.
	 */
	@Override
	public byte[] getEncoded() {
		if (encoding == null) {
			encoding = ASN1.encode(this);
		}
		return encoding;
	}

	//
	// Returns the value of certificate extension
	//
	private byte[] getExtensionValue(X509Certificate cert, String OID) {
		try {
			final byte[] bytes = cert.getExtensionValue(OID);
			if (bytes == null) {
				return null;
			}
			return (byte[]) ASN1OctetString.getInstance().decode(bytes);
		} catch (final IOException e) {
			return null;
		}
	}

	/**
	 * Check if this list of names is acceptable accoring to this
	 * NameConstraints object.
	 * 
	 * @param names
	 *            : List
	 * @return
	 */
	public boolean isAcceptable(List<GeneralName> names) {
		if (permitted_names == null) {
			prepareNames();
		}

		final Iterator<GeneralName> it = names.iterator();
		// check map: shows which types of permitted alternative names are
		// presented in the certificate
		final boolean[] types_presented = new boolean[9];
		// check map: shows if permitted name of presented type is found
		// among the certificate's alternative names
		final boolean[] permitted_found = new boolean[9];
		while (it.hasNext()) {
			final GeneralName name = it.next();
			final int type = name.getTag();
			// search the name in excluded names
			if (excluded_names[type] != null) {
				for (int i = 0; i < excluded_names[type].size(); i++) {
					if (excluded_names[type].get(i).isAcceptable(name)) {
						return false;
					}
				}
			}
			// Search the name in permitted names
			// (if we already found the name of such type between the alt
			// names - we do not need to check others)
			if ((permitted_names[type] != null) && (!permitted_found[type])) {
				types_presented[type] = true;
				for (int i = 0; i < permitted_names[type].size(); i++) {
					if (permitted_names[type].get(i).isAcceptable(name)) {
						// found one permitted name of such type
						permitted_found[type] = true;
					}
				}
			}
		}
		for (int type = 0; type < 9; type++) {
			if (types_presented[type] && !permitted_found[type]) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Apply the name restrictions specified by this NameConstraints instance to
	 * the subject distinguished name and subject alternative names of specified
	 * X509Certificate. Restrictions apply only if specified name form is
	 * present in the certificate. The restrictions are applied according the
	 * RFC 3280 (see 4.2.1.11 Name Constraints), excepting that restrictions are
	 * applied and to CA certificates, and to certificates which issuer and
	 * subject names the same (i.e. method does not check if it CA's certificate
	 * or not, or if the names differ or not. This check if it is needed should
	 * be done by caller before calling this method).
	 * 
	 * @param X509Certificate
	 *            : X.509 Certificate to be checked.
	 * @return true, if the certificate is acceptable according these
	 *         NameConstraints restrictions, and false otherwise.
	 */
	public boolean isAcceptable(X509Certificate cert) {
		if (permitted_names == null) {
			prepareNames();
		}

		final byte[] bytes = getExtensionValue(cert, "2.5.29.17"); //$NON-NLS-1$
		List<GeneralName> names;
		try {
			names = (bytes == null) ? new ArrayList<GeneralName>(1) // will
																	// check the
																	// subject
					// field only
					: ((GeneralNames) GeneralNames.ASN1.decode(bytes))
							.getNames();
		} catch (final IOException e) {
			// the certificate is broken;
			e.printStackTrace();
			return false;
		}
		if ((excluded_names[4] != null) || (permitted_names[4] != null)) {
			try {
				names.add(new GeneralName(4, cert.getSubjectX500Principal()
						.getName()));
			} catch (final IOException e) {
				// should never be happened
			}
		}
		return isAcceptable(names);
	}

	//
	// Prepare the data structure to speed up the checking process.
	//
	private void prepareNames() {
		// array of lists with permitted General Names divided by type
		permitted_names = new ArrayList[9];
		if (permittedSubtrees != null) {
			final Iterator<?> it = permittedSubtrees.getSubtrees().iterator();
			while (it.hasNext()) {
				final GeneralName name = ((GeneralSubtree) it.next()).getBase();
				// System.out.println("PERMITTED: "+name);
				final int tag = name.getTag();
				if (permitted_names[tag] == null) {
					permitted_names[tag] = new ArrayList<GeneralName>();
				}
				permitted_names[tag].add(name);
			}
		}
		// array of lists with excluded General Names divided by type
		excluded_names = new ArrayList[9];
		if (excludedSubtrees != null) {
			final Iterator<?> it = excludedSubtrees.getSubtrees().iterator();
			while (it.hasNext()) {
				final GeneralName name = ((GeneralSubtree) it.next()).getBase();
				// System.out.println("EXCLUDED: "+name);
				final int tag = name.getTag();
				if (excluded_names[tag] == null) {
					excluded_names[tag] = new ArrayList<GeneralName>();
				}
				excluded_names[tag].add(name);
			}
		}
	}
}
